package org.lex.pci.ch02;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.lex.pci.ch02.data.Preferences;
import org.lex.pci.ch02.data.Preferences.Rate;
import org.lex.pci.ch02.similarity.PearsonSimilarityAlgorithm;
import org.lex.pci.ch02.similarity.SimilarityAlgorithm;
import org.lex.pci.common.Algorithm;

public class RecommendEngine implements Algorithm {
	private static final int DEFAULT_TOP_COUNT = 5;
	private static final SimilarityAlgorithm DEFAULT_ALGORITHM = new PearsonSimilarityAlgorithm();

	@Override
	public String getName() {
		return "Recommend Engine";
	}

	public List<Match> getTopMatches(Preferences prefs, String user) {
		return this.getTopMatches(prefs, user, DEFAULT_TOP_COUNT, DEFAULT_ALGORITHM);
	}

	public List<Match> getTopMatches(Preferences prefs, String user, int topCount, SimilarityAlgorithm algorithm) {
		Collection<String> allUsers = prefs.getAllUsers();
		List<Match> allMatches = new ArrayList<Match>(allUsers.size() - 1);
		for (String other : allUsers) {
			if (false == user.equals(other)) {
				double sim = algorithm.getSimilarity(prefs, user, other);
				Match match = new Match(other, sim);
				allMatches.add(match);
			}
		}
		Collections.sort(allMatches);
		if (allMatches.size() > topCount) {
			allMatches = allMatches.subList(0, topCount);
		}
		return allMatches;
	}

	// ==========================

	public List<Recommendation> getRecommendations(Preferences prefs, String person, SimilarityAlgorithm algorithm) {
		Map<String, Double> totals = new HashMap<String, Double>();
		Map<String, Double> simSums = new HashMap<String, Double>();

		List<Rate> meRates = prefs.getRates(person);
		Collection<String> allUsers = prefs.getAllUsers();
		for (String other : allUsers) {
			if (person.equals(other)) {
				continue;
			}

			double sim = algorithm.getSimilarity(prefs, person, other);
			if (sim <= 0) {
				continue;
			}

			List<Rate> otherRates = prefs.getRates(other);
			for (Rate r : otherRates) {
				String currentMovieName = r.getMovie();
				if (null == Rate.findFirstByMovie(meRates, currentMovieName)) {
					Double total = totals.get(currentMovieName);
					if (null == total) {
						total = 0d;
					}
					total += r.getRate() * sim;
					totals.put(currentMovieName, total);

					Double simSum = simSums.get(currentMovieName);
					if (null == simSum) {
						simSum = 0d;
					}
					simSum += sim;
					simSums.put(currentMovieName, simSum);
				}
			}
		}

		List<Recommendation> recommendations = new ArrayList<Recommendation>();
		for (String movie : totals.keySet()) {
			double total = totals.get(movie);
			double simSum = simSums.get(movie);
			recommendations.add(new Recommendation(movie, total / simSum));
		}
		Collections.sort(recommendations);
		return recommendations;
	}

	public static final class Recommendation implements Comparable<Recommendation> {
		private String movie;
		private double rate;

		public Recommendation(String movie, double rate) {
			super();
			this.movie = movie;
			this.rate = rate;
		}

		public String getMovie() {
			return movie;
		}

		public void setMovie(String movie) {
			this.movie = movie;
		}

		public double getRate() {
			return rate;
		}

		public void setRate(double rate) {
			this.rate = rate;
		}

		@Override
		public int compareTo(Recommendation o) {
			return -1 * Double.valueOf(this.rate).compareTo(o.rate);
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((movie == null) ? 0 : movie.hashCode());
			long temp;
			temp = Double.doubleToLongBits(rate);
			result = prime * result + (int) (temp ^ (temp >>> 32));
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			Recommendation other = (Recommendation) obj;
			if (movie == null) {
				if (other.movie != null)
					return false;
			} else if (!movie.equals(other.movie))
				return false;
			if (Double.doubleToLongBits(rate) != Double.doubleToLongBits(other.rate))
				return false;
			return true;
		}

		@Override
		public String toString() {
			return "Recommendation [movie=" + movie + ", rate=" + rate + "]";
		}
	}

	public static final class Match implements Comparable<Match> {
		private double similarity = 0;
		private String person = null;

		public Match(String person1, double distance) {
			super();
			this.person = person1;
			this.similarity = distance;
		}

		public double getSimilarity() {
			return similarity;
		}

		public void setSimilarity(double distance) {
			this.similarity = distance;
		}

		public String getPerson() {
			return person;
		}

		public void setPerson(String person) {
			this.person = person;
		}

		@Override
		public int compareTo(Match o) {
			return -1 * Double.valueOf(this.similarity).compareTo(o.similarity);
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			long temp;
			temp = Double.doubleToLongBits(similarity);
			result = prime * result + (int) (temp ^ (temp >>> 32));
			result = prime * result + ((person == null) ? 0 : person.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			Match other = (Match) obj;
			if (Double.doubleToLongBits(similarity) != Double.doubleToLongBits(other.similarity))
				return false;
			if (person == null) {
				if (other.person != null)
					return false;
			} else if (!person.equals(other.person))
				return false;
			return true;
		}

		@Override
		public String toString() {
			return "Match [similarity=" + similarity + ", person=" + person + "]";
		}

	}
}
